Skip to content

[Server] Build tool schemas with symfony/json-schema - #740

Draft
chr-hertel wants to merge 2 commits into
modelcontextprotocol:mainfrom
chr-hertel:json-schema-component
Draft

chr-hertel wants to merge 2 commits into
modelcontextprotocol:mainfrom
chr-hertel:json-schema-component

Conversation

@chr-hertel

@chr-hertel chr-hertel commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

Replaces our schema generator with the new JsonSchema component (symfony/json-schema 8.2.0-BETA1). TypeInfo resolves the parameter types, so the string parsing in SchemaGenerator can go (961 => ~190 lines). The second commit is by Nicolas and describes inputs and outputs with their direction.

  • a class parameter is described by its properties, incl. validator constraints, instead of a bare {type: object}
  • stays on our side: injectable parameters, the reserved _session/_request names and #[Schema]
  • output schemas from return types are opt-in (outputSchemaFromReturnType) - structuredContent is still built with json_encode(), which doesn't match what the component describes for getters or DateTimeImmutable
  • class-typed arguments still arrive as arrays - hydrating them is a follow up
  • BC breaks are in the CHANGELOG under 0.11.0, and the Symfony 5.4/6.4 jobs are gone since the component needs 7.4+
  • apps need minimum-stability: beta until the component is stable

array<int, V> and array<array-key, V> are described as a JSON array now, so the object form #643 added for them is gone - that's a BC break in the CHANGELOG, use array<string, V> for a map. TypeInfo can't tell V[] from array<array-key, V>, and lists are what these types mean in practice. I also dropped the tests that only covered the old type-string parser.

What we lose: apps that use the Symfony 6.4 Serializer, Validator or PropertyInfo, or pin all Symfony packages to 6.4 via Flex, can't upgrade - the component needs 7.4+ of those. Array shapes are closed now (additionalProperties: false), so extra keys in a shape argument get -32602 - the rest is in the CHANGELOG.

@soyuka could you have a look? 🙏

@chr-hertel
chr-hertel force-pushed the json-schema-component branch from 32762e3 to 07b0407 Compare November 11, 2026 23:38
chr-hertel and others added 2 commits November 12, 2026 02:08
SchemaGenerator describes the parameters of a handler with the component's generateForParameters() instead of parsing type strings itself. It keeps the MCP-specific parts: injectable parameters, reserved parameter names and the #[Schema] attribute.
Input schemas only list the properties a client can send (Direction::Input), and output schemas require the properties that are always present (Direction::Output). A dialect without schema URI makes stripping $schema unnecessary, and definitions keep the plain class name since each schema is a document of its own.
@chr-hertel
chr-hertel force-pushed the json-schema-component branch from 07b0407 to 0a684e5 Compare November 12, 2026 01:09
@chr-hertel chr-hertel added this to the 1.0.0 milestone Nov 12, 2026
@chr-hertel chr-hertel added the Server Issues & PRs related to the Server component label Nov 12, 2026

@soyuka soyuka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing!

if (isset($parameterLevelSchema['definition']) && \is_array($parameterLevelSchema['definition'])) {
$mergedSchema = $parameterLevelSchema['definition'];
foreach ($parameters as $name => $parameter) {
$property = (array) $schema['properties'][$name];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component skips callable and resource parameters (isRepresentable() in buildParametersSchema), so the key isn't there. With cb(string $a, ?callable $fn = null) I get Undefined array key "fn" and "fn":{} ends up in the schema, which accepts anything for an argument JSON can't carry. Can we skip the parameter when the component didn't emit it?

?JsonSchemaGeneratorInterface $generator = null,
?TypeResolverInterface $typeResolver = null,
private readonly Configuration $configuration = new Configuration(new Dialect('#/$defs/', NullSyntax::Union), definitionName: '', direction: Direction::Input),
private readonly bool $outputSchemaFromReturnType = false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a getter and a DateTimeImmutable the schema says {"at": {"type": "string", "format": "date-time"}, "name": ...} (both required) but json_encode() gives {"at": {"date": ..., "timezone": ...}} without name. Since #515 validates structuredContent against outputSchema, turning this on makes these tools fail at call time, and that's the most common class shape. I'd either build structuredContent with a normalizer when the flag is on (that's how we keep Direction::Output true in API Platform) or leave the flag out of 0.11. Also ?Foo gives no schema at all since it's a NullableType, fine but worth a note in the docblock.

Comment thread composer.json
"symfony/deprecation-contracts": "^2.5 || ^3.0",
"symfony/json-schema": "^8.2@beta",
"symfony/type-info": "^7.4.6 || ^8.0.6",
"symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

property-info ^7.4 makes 5.4/6.4 installs impossible now, can we bump the remaining ^5.4 || ^6.4 ranges (here, finder, http-client, lock, process...) to match what CI tests, and SYMFONY_REQUIRE still defaults to '>=6.4' in the pipeline?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Server Issues & PRs related to the Server component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants